/**
 * 
 */
package uk.co.altv.simpledb;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimeZone;
import java.util.logging.Logger;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

import uk.co.altv.simpledb.coder.ICoder;
import uk.co.altv.simpledb.coder.SimpleURLCoder;
import uk.co.altv.simpledb.connector.IConnector;
import uk.co.altv.simpledb.connector.SimpleURLConnector;
import uk.co.altv.simpledb.operation.IOperation;


/**
 * @author niki
 * 
 */
public class Connection {
	private static final Logger log = Logger.getLogger(Connection.class
			.getName());
	private String accessKey;
	private SecretKeySpec secret;
	private String scheme;
	private String endpoint;

	/**
	 * @param accessKey
	 * @param secret
	 * @param scheme
	 * @param endpoint
	 * @throws InitialisationException
	 */
	public Connection(String accessKey, SecretKeySpec secret, String scheme,
			String endpoint) throws InitialisationException {
		this.accessKey = accessKey;
		this.secret = secret;
		this.scheme = scheme;
		this.endpoint = endpoint;
		this.sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
		try {
			this.mac = Mac.getInstance("HmacSHA256");
		} catch (NoSuchAlgorithmException e) {
			log
					.severe("Connection:"
							+ this.endpoint
							+ " encountered NoSuchAlgorithmException on initialisation of HmacSHA256");
			throw new InitialisationException(e);
		}
		try {
			mac.init(secret);
		} catch (InvalidKeyException e) {
			String error = "Connection:" + this.endpoint
					+ " method:sign(String) -- InvalidKeyException";
			log.severe(error);
			throw new InitialisationException(e);
		}
	}

	private final Mac mac;
	private final SimpleDateFormat sdf = new SimpleDateFormat(
			"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
	private ICoder urlCoder = new SimpleURLCoder();
	private IConnector urlConnector = new SimpleURLConnector();
	private String signatureMethod = "HmacSHA256";
	private String signatureVersion = "2";
	private String version = "2009-04-15";
	private String requestURI = "/";
	private boolean doPost = true;
	private ICoder base64Coder = null;

	public ICoder getBase64Coder() {
		return base64Coder;
	}
	public void setBase64Coder(ICoder base64Coder) {
		this.base64Coder = base64Coder;
	}
	/**
	 * @param operation
	 * @return a wrapper for the db reply
	 * @throws IOException 
	 */
	public IReply submit(IOperation operation) throws IOException {
		IReply reply = this.process(operation);
		// Once it's been submitted once, check if it's something we need to
		// still hammer in
		while (operation.hasMoreItems()) {
				reply = this.process(operation);
		}
		return reply;
	}
	protected IReply process(IOperation operation) throws IOException  {
		if (isDoPost()) {
			if (urlConnector.canDoPost()) {
				final URL url = new URL(this.scheme + this.endpoint
						+ this.requestURI);
				final String postBody = operation.prepareStatement(this);
				return urlConnector.doPost(url, postBody);
			} else {
				log
						.warning("Connection: "
								+ this.endpoint
								+ " method:submit() -- urlConnector does not implement http POST, try GET. Operation not submitted");
			}

		} else {
			if (urlConnector.canDoGet()) {
				return urlConnector.doGet(new URL(this.scheme + this.endpoint
						+ this.requestURI + "?"
						+ operation.prepareStatement(this)));
			} else {
				log
						.warning("Connection: "
								+ this.endpoint
								+ " method:submit() -- urlConnector does not implement http GET, try POST. Operation not submitted");
			}
		}
		return null;
	}
	/**
	 * @param value
	 * @return
	 */
	public String encode(String value) {
		if (urlCoder.canEncode(value)) {
			return (String) urlCoder.encode(value);
		} else {
			String error = "Connection:" + this.endpoint
					+ " method:encode( --urlCoder:"
					+ this.urlCoder.getClass().getName()
					+ " does not implement encode method";
			log.severe(error);
			// must bail as this will probably cause problems if allowed to
			// continue
			throw new RuntimeException(error);
		}
	}

	/**
	 * @param statement
	 * @return
	 */
	public String sign(String statement) {
		try {
			String prep = (isDoPost() ? "POST\n" : "GET\n")
				+ endpoint + "\n" + requestURI + "\n" + statement;
			byte[] signature = mac.doFinal(prep.getBytes("utf-8"));
			if(base64Coder.canEncode(signature)) {
				return (String) base64Coder.encode(signature);
			} else {
				String error = ""; // TODO
				new RuntimeException(error);
			}
		} catch (IllegalStateException e) {
			log.severe("Mac not initialised");
			throw new RuntimeException(e);
		} catch (UnsupportedEncodingException e) {
			log.severe("UnsupportedEncodingException while calculating signature");
			throw new RuntimeException(e);
		}
		return "";
	}

	/**
	 * @return
	 */
	public synchronized String getTimestamp() {
		return sdf.format(new Date());
	}

	/**
	 * @return the signatureMethod
	 */
	public String getSignatureMethod() {
		return signatureMethod;
	}

	/**
	 * @param signatureMethod
	 *            the signatureMethod to set
	 */
	public void setSignatureMethod(String signatureMethod) {
		this.signatureMethod = signatureMethod;
	}

	/**
	 * @return the doPost
	 */
	public boolean isDoPost() {
		return doPost;
	}

	/**
	 * @param doPost
	 *            the doPost to set
	 */
	public void setDoPost(boolean doPost) {
		this.doPost = doPost;
	}

	/**
	 * @return the signatureVersion
	 */
	public String getSignatureVersion() {
		return signatureVersion;
	}

	/**
	 * @param signatureVersion
	 *            the signatureVersion to set
	 */
	public void setSignatureVersion(String signatureVersion) {
		this.signatureVersion = signatureVersion;
	}

	/**
	 * @return the version
	 */
	public String getVersion() {
		return version;
	}

	/**
	 * @param version
	 *            the version to set
	 */
	public void setVersion(String version) {
		this.version = version;
	}

	/**
	 * @return the accessKey
	 */
	public String getAccessKey() {
		return accessKey;
	}

	/**
	 * @param accessKey
	 *            the accessKey to set
	 */
	public void setAccessKey(String accessKey) {
		this.accessKey = accessKey;
	}

	/**
	 * @return the secret
	 */
	public SecretKeySpec getSecret() {
		return secret;
	}

	/**
	 * @param secret
	 *            the secret to set
	 */
	public void setSecret(SecretKeySpec secret) {
		this.secret = secret;
		try {
			mac.init(secret);
		} catch (InvalidKeyException e) {
			String error = "Connection:"
					+ this.endpoint
					+ " method:setSecret(SecretKeySpec) -- InvalidKeyException -- was the key changed at runtime?";
			log.severe(error);
			throw new RuntimeException(error, e);
		}
	}

	/**
	 * @return the scheme
	 */
	public String getScheme() {
		return scheme;
	}

	/**
	 * @param scheme
	 *            the scheme to set
	 */
	public void setScheme(String scheme) {
		this.scheme = scheme;
	}

	/**
	 * @return the endpoint
	 */
	public String getEndpoint() {
		return endpoint;
	}

	/**
	 * @param endpoint
	 *            the endpoint to set
	 */
	public void setEndpoint(String endpoint) {
		this.endpoint = endpoint;
	}

	/**
	 * @return the urlCoder
	 */
	public ICoder getURLCoder() {
		return urlCoder;
	}

	/**
	 * @param urlCoder
	 *            the urlCoder to set
	 */
	public void setURLCoder(ICoder urlCoder) {
		this.urlCoder = urlCoder;
	}

	/**
	 * @return the urlConnector
	 */
	public IConnector getURLConnector() {
		return urlConnector;
	}

	/**
	 * @param urlConnector
	 *            the urlConnector to set
	 */
	public void setURLConnector(IConnector urlConnector) {
		this.urlConnector = urlConnector;
	}

	/**
	 * @return the requestURI
	 */
	public String getRequestURI() {
		return requestURI;
	}

	/**
	 * @param requestURI
	 *            the requestURI to set
	 */
	public void setRequestURI(String requestURI) {
		this.requestURI = requestURI;
	}

	/**
	 * @param domain
	 * @return
	 */
	public String validateDomainName(String domain) throws ValidationException {
		// TODO Auto-generated method stub
		return domain;
	}
}
